/*-------------------------------------------------------------------------
 *
 * expandedrecord.c
 *	  操作复合扩展对象的函数。
 *
 * 本模块支持 "扩展对象"（参见 expandeddatum.h），可以
 * 存储命名复合类型、命名复合类型的领域
 * 以及记录类型（注册或匿名）的值。
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 *
 * IDENTIFICATION
 *	  src/backend/utils/adt/expandedrecord.c
 *
 *-------------------------------------------------------------------------
 */
#include "postgres.h"

#include "access/detoast.h"
#include "access/heaptoast.h"
#include "access/htup_details.h"
#include "catalog/heap.h"
#include "catalog/pg_type.h"
#include "utils/builtins.h"
#include "utils/datum.h"
#include "utils/expandedrecord.h"
#include "utils/memutils.h"
#include "utils/typcache.h"


/* 扩展对象所需的 "方法" */
static Size fc_ER_get_flat_size(ExpandedObjectHeader *fc_eohptr);
static void fc_ER_flatten_into(ExpandedObjectHeader *fc_eohptr,
							void *fc_result, Size fc_allocated_size);

static const ExpandedObjectMethods ER_methods =
{
	fc_ER_get_flat_size,
	fc_ER_flatten_into
};

/* 其他本地函数 */
static void fc_ER_mc_callback(void *fc_arg);
static MemoryContext get_short_term_cxt(ExpandedRecordHeader *fc_erh);
static void fc_build_dummy_expanded_header(ExpandedRecordHeader *fc_main_erh);
static pg_noinline void fc_check_domain_for_new_field(ExpandedRecordHeader *fc_erh,
												   int fc_fnumber,
												   Datum fc_newValue, bool fc_isnull);
static pg_noinline void fc_check_domain_for_new_tuple(ExpandedRecordHeader *fc_erh,
												   HeapTuple fc_tuple);


/*
 * 构建指定复合类型的扩展记录
 *
 * type_id 可以是 RECORDOID，但只有在给定正的 typmod 时。
 *
 * 扩展记录最初是“空”的，其状态在逻辑上等同于 NULL 复合值（不是 ROW(NULL, NULL,...)）。
 * 注意，这对于域类型可能不是有效状态；
 * 如果调用者需要检查，可以调用
 * expanded_record_set_tuple(erh, NULL, false, false)。
 *
 * 扩展对象将是 parentcontext 的子对象。
 */
ExpandedRecordHeader * make_expanded_record_from_typeid(Oid fc_type_id, int32 fc_typmod,
								 MemoryContext fc_parentcontext)
{
	ExpandedRecordHeader *fc_erh;
	int			fc_flags = 0;
	TupleDesc	fc_tupdesc;
	uint64		fc_tupdesc_id;
	MemoryContext fc_objcxt;
	char	   *fc_chunk;

	if (fc_type_id != RECORDOID)
	{
		/*
		 * 查询 typcache 以查看它是否是复合的域，并且
		 * 在任何情况下获取 tupdesc 和 tupdesc 标识符。
		 */
		TypeCacheEntry *fc_typentry;

		fc_typentry = lookup_type_cache(fc_type_id,
									 TYPECACHE_TUPDESC |
									 TYPECACHE_DOMAIN_BASE_INFO);
		if (fc_typentry->typtype == TYPTYPE_DOMAIN)
		{
			fc_flags |= ER_FLAG_IS_DOMAIN;
			fc_typentry = lookup_type_cache(fc_typentry->domainBaseType,
										 TYPECACHE_TUPDESC);
		}
		if (fc_typentry->tupDesc == NULL)
			ereport(ERROR,
					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
					 errmsg("type %s is not composite",
							format_type_be(fc_type_id))));
		fc_tupdesc = fc_typentry->tupDesc;
		fc_tupdesc_id = fc_typentry->tupDesc_identifier;
	}
	else
	{
		/*
		 * 对于 RECORD 类型，从 typcache 获取 tupdesc 和标识符。
		 */
		fc_tupdesc = lookup_rowtype_tupdesc(fc_type_id, fc_typmod);
		fc_tupdesc_id = assign_record_type_identifier(fc_type_id, fc_typmod);
	}

	/*
	 * 为扩展对象分配私有上下文。我们使用常规大小的
	 * 上下文，而不是小型上下文，以提高我们能够在其中容纳 tupdesc
	 * 而无需额外的 malloc 块的几率。（此代码路径从不
	 * 需要将 tupdesc 复制到扩展记录中，但让我们与其他创建扩展记录的方法
	 * 保持一致。）
	 */
	fc_objcxt = AllocSetContextCreate(fc_parentcontext,
								   "expanded record",
								   ALLOCSET_DEFAULT_SIZES);

	/*
	 * 由于我们已经知道 tupdesc 中字段的数量，我们可以
	 * 与记录头一起分配 dvalues/dnulls 数组。这
	 * 在我们从未需要这些数组的情况下是没有用的，但成本几乎为零，
	 * 如果我们确实需要它们，将节省一个 palloc 周期。
	 */
	fc_erh = (ExpandedRecordHeader *)
		MemoryContextAlloc(fc_objcxt, MAXALIGN(sizeof(ExpandedRecordHeader))
						   + fc_tupdesc->natts * (sizeof(Datum) + sizeof(bool)));

	/* 确保所有头字段初始化为 0/null */
	memset(fc_erh, 0, sizeof(ExpandedRecordHeader));

	EOH_init_header(&fc_erh->hdr, &ER_methods, fc_objcxt);
	fc_erh->er_magic = ER_MAGIC;

	/* 设置 dvalues/dnulls，目前没有有效内容 */
	fc_chunk = (char *) fc_erh + MAXALIGN(sizeof(ExpandedRecordHeader));
	fc_erh->dvalues = (Datum *) fc_chunk;
	fc_erh->dnulls = (bool *) (fc_chunk + fc_tupdesc->natts * sizeof(Datum));
	fc_erh->nfields = fc_tupdesc->natts;

	/* 填充复合类型识别信息 */
	fc_erh->er_decltypeid = fc_type_id;
	fc_erh->er_typeid = fc_tupdesc->tdtypeid;
	fc_erh->er_typmod = fc_tupdesc->tdtypmod;
	fc_erh->er_tupdesc_id = fc_tupdesc_id;

	fc_erh->flags = fc_flags;

	/*
	 * 如果我们从 typcache 获取的对象是引用计数的 tupdesc，我们需要
	 * 获得自己的引用计数。我们通过内存
	 * 上下文回调来管理引用计数，而不是假设 CurrentResourceOwner
	 * 的生命期长于此扩展对象。
	 */
	if (fc_tupdesc->tdrefcount >= 0)
	{
		/* 注册回调以释放引用计数 */
		fc_erh->er_mcb.func = fc_ER_mc_callback;
		fc_erh->er_mcb.arg = (void *) fc_erh;
		MemoryContextRegisterResetCallback(fc_erh->hdr.eoh_context,
										   &fc_erh->er_mcb);

		/* 并保存指针 */
		fc_erh->er_tupdesc = fc_tupdesc;
		fc_tupdesc->tdrefcount++;

		/* 如果我们调用了 lookup_rowtype_tupdesc，释放所占用的引脚 */
		if (fc_type_id == RECORDOID)
			ReleaseTupleDesc(fc_tupdesc);
	}
	else
	{
		/*
		 * 如果它不是引用计数的，只需假设它会超过扩展
		 * 对象的生存期。（例如，共享记录类型可能发生这种情况。）
		 */
		fc_erh->er_tupdesc = fc_tupdesc;
	}

	/*
	 * 我们不设置 ER_FLAG_DVALUES_VALID 或 ER_FLAG_FVALUE_VALID，因此
	 * 记录在逻辑上仍然是空的。
	 */

	return fc_erh;
}

/*
 * 构建与 tupdesc 定义的行类型相同的扩展记录
 *
 * 如果必要，tupdesc 将被复制（即，如果我们不能简单地增加其
 * 引用计数）。
 *
 * 扩展记录最初是“空”的，其状态在逻辑上等同于 NULL 复合值（不是 ROW(NULL, NULL,...)）。
 *
 * 扩展对象将是 parentcontext 的子对象。
 */
ExpandedRecordHeader * make_expanded_record_from_tupdesc(TupleDesc fc_tupdesc,
								  MemoryContext fc_parentcontext)
{
	ExpandedRecordHeader *fc_erh;
	uint64		fc_tupdesc_id;
	MemoryContext fc_objcxt;
	MemoryContext fc_oldcxt;
	char	   *fc_chunk;

	if (fc_tupdesc->tdtypeid != RECORDOID)
	{
		/*
		 * 如果它是命名的复合类型（而不是 RECORD），我们更喜欢引用
		 * typcache 的 tupdesc 副本，该副本保证是
		 * 引用计数的（给定的 tupdesc 可能不是）。在任何情况下，我们需要
		 * 查询 typcache 以获取正确的 tupdesc 标识符。
		 *
		 * 注意，tdtypeid 不能是域类型，因此我们在这里无需
		 * 考虑这种情况。
		 */
		TypeCacheEntry *fc_typentry;

		fc_typentry = lookup_type_cache(fc_tupdesc->tdtypeid, TYPECACHE_TUPDESC);
		if (fc_typentry->tupDesc == NULL)
			ereport(ERROR,
					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
					 errmsg("type %s is not composite",
							format_type_be(fc_tupdesc->tdtypeid))));
		fc_tupdesc = fc_typentry->tupDesc;
		fc_tupdesc_id = fc_typentry->tupDesc_identifier;
	}
	else
	{
		/*
		 * 对于 RECORD 类型，获取适当的唯一标识符（可能是
		 * 新近分配的）。
		 */
		fc_tupdesc_id = assign_record_type_identifier(fc_tupdesc->tdtypeid,
												   fc_tupdesc->tdtypmod);
	}

	/*
	 * 为扩展对象分配私有上下文。我们使用常规大小的
	 * 上下文，而不是小型上下文，以提高我们能够在其中容纳 tupdesc
	 * 而无需额外的 malloc 块的几率。
	 */
	fc_objcxt = AllocSetContextCreate(fc_parentcontext,
								   "expanded record",
								   ALLOCSET_DEFAULT_SIZES);

	/*
	 * 由于我们已经知道 tupdesc 中字段的数量，我们可以
	 * 与记录头一起分配 dvalues/dnulls 数组。这
	 * 在我们从未需要这些数组的情况下是没有用的，但成本几乎为零，
	 * 如果我们确实需要它们，将节省一个 palloc 周期。
	 */
	fc_erh = (ExpandedRecordHeader *)
		MemoryContextAlloc(fc_objcxt, MAXALIGN(sizeof(ExpandedRecordHeader))
						   + fc_tupdesc->natts * (sizeof(Datum) + sizeof(bool)));

	/* 确保所有头字段初始化为 0/null */
	memset(fc_erh, 0, sizeof(ExpandedRecordHeader));

	EOH_init_header(&fc_erh->hdr, &ER_methods, fc_objcxt);
	fc_erh->er_magic = ER_MAGIC;

	/* 设置 dvalues/dnulls，目前没有有效内容 */
	fc_chunk = (char *) fc_erh + MAXALIGN(sizeof(ExpandedRecordHeader));
	fc_erh->dvalues = (Datum *) fc_chunk;
	fc_erh->dnulls = (bool *) (fc_chunk + fc_tupdesc->natts * sizeof(Datum));
	fc_erh->nfields = fc_tupdesc->natts;

	/* 填充复合类型识别信息 */
	fc_erh->er_decltypeid = fc_erh->er_typeid = fc_tupdesc->tdtypeid;
	fc_erh->er_typmod = fc_tupdesc->tdtypmod;
	fc_erh->er_tupdesc_id = fc_tupdesc_id;

	/*
	 * 如果需要，则复制 tupdesc，但如果可能，我们更喜欢增加其引用计数。
	 * 我们通过内存上下文回调来管理引用计数，而不是假设
	 * CurrentResourceOwner 的生命期长于此
	 * 扩展对象。
	 */
	if (fc_tupdesc->tdrefcount >= 0)
	{
		/* 注册回调以释放引用计数 */
		fc_erh->er_mcb.func = fc_ER_mc_callback;
		fc_erh->er_mcb.arg = (void *) fc_erh;
		MemoryContextRegisterResetCallback(fc_erh->hdr.eoh_context,
										   &fc_erh->er_mcb);

		/* 并保存指针 */
		fc_erh->er_tupdesc = fc_tupdesc;
		fc_tupdesc->tdrefcount++;
	}
	else
	{
		/* 直接复制 */
		fc_oldcxt = MemoryContextSwitchTo(fc_objcxt);
		fc_erh->er_tupdesc = CreateTupleDescCopy(fc_tupdesc);
		fc_erh->flags |= ER_FLAG_TUPDESC_ALLOCED;
		MemoryContextSwitchTo(fc_oldcxt);
	}

	/*
	 * 我们不设置 ER_FLAG_DVALUES_VALID 或 ER_FLAG_FVALUE_VALID，因此
	 * 记录在逻辑上仍然是空的。
	 */

	return fc_erh;
}

/*
 * 构建与给定扩展记录相同行类型的扩展记录
 *
 * 这比上述任一例程都快，因为我们可以绕过
 * typcache 查找。
 *
 * 扩展记录最初是“空”的——我们不复制任何
 * 可能存在于源扩展记录中的元组。
 *
 * 扩展对象将是 parentcontext 的子对象。
 */
ExpandedRecordHeader * make_expanded_record_from_exprecord(ExpandedRecordHeader *fc_olderh,
									MemoryContext fc_parentcontext)
{
	ExpandedRecordHeader *fc_erh;
	TupleDesc	fc_tupdesc = expanded_record_get_tupdesc(fc_olderh);
	MemoryContext fc_objcxt;
	MemoryContext fc_oldcxt;
	char	   *fc_chunk;

	/*
	 * 为扩展对象分配私有上下文。我们使用常规大小的
	 * 上下文，而不是小型上下文，以提高我们能够在其中容纳 tupdesc
	 * 而无需额外的 malloc 块的几率。
	 */
	fc_objcxt = AllocSetContextCreate(fc_parentcontext,
								   "expanded record",
								   ALLOCSET_DEFAULT_SIZES);

	/*
	 * 由于我们已经知道 tupdesc 中字段的数量，我们可以
	 * 与记录头一起分配 dvalues/dnulls 数组。这
	 * 在我们从未需要这些数组的情况下是没有用的，但成本几乎为零，
	 * 如果我们确实需要它们，将节省一个 palloc 周期。
	 */
	fc_erh = (ExpandedRecordHeader *)
		MemoryContextAlloc(fc_objcxt, MAXALIGN(sizeof(ExpandedRecordHeader))
						   + fc_tupdesc->natts * (sizeof(Datum) + sizeof(bool)));

	/* 确保所有头字段初始化为 0/null */
	memset(fc_erh, 0, sizeof(ExpandedRecordHeader));

	EOH_init_header(&fc_erh->hdr, &ER_methods, fc_objcxt);
	fc_erh->er_magic = ER_MAGIC;

	/* 设置 dvalues/dnulls，目前没有有效内容 */
	fc_chunk = (char *) fc_erh + MAXALIGN(sizeof(ExpandedRecordHeader));
	fc_erh->dvalues = (Datum *) fc_chunk;
	fc_erh->dnulls = (bool *) (fc_chunk + fc_tupdesc->natts * sizeof(Datum));
	fc_erh->nfields = fc_tupdesc->natts;

	/* 填充复合类型识别信息 */
	fc_erh->er_decltypeid = fc_olderh->er_decltypeid;
	fc_erh->er_typeid = fc_olderh->er_typeid;
	fc_erh->er_typmod = fc_olderh->er_typmod;
	fc_erh->er_tupdesc_id = fc_olderh->er_tupdesc_id;

	/* 唯一转移的标志位是 IS_DOMAIN */
	fc_erh->flags = fc_olderh->flags & ER_FLAG_IS_DOMAIN;

	/*
	 * 如果需要，则复制 tupdesc，但如果可能，我们更喜欢增加其引用计数。
	 * 我们通过内存上下文回调来管理引用计数，而不是假设
	 * CurrentResourceOwner 的生命期长于此
	 * 扩展对象。
	 */
	if (fc_tupdesc->tdrefcount >= 0)
	{
		/* 注册回调以释放引用计数 */
		fc_erh->er_mcb.func = fc_ER_mc_callback;
		fc_erh->er_mcb.arg = (void *) fc_erh;
		MemoryContextRegisterResetCallback(fc_erh->hdr.eoh_context,
										   &fc_erh->er_mcb);

		/* 并保存指针 */
		fc_erh->er_tupdesc = fc_tupdesc;
		fc_tupdesc->tdrefcount++;
	}
	else if (fc_olderh->flags & ER_FLAG_TUPDESC_ALLOCED)
	{
		/* 我们需要自己复制 tupdesc */
		fc_oldcxt = MemoryContextSwitchTo(fc_objcxt);
		fc_erh->er_tupdesc = CreateTupleDescCopy(fc_tupdesc);
		fc_erh->flags |= ER_FLAG_TUPDESC_ALLOCED;
		MemoryContextSwitchTo(fc_oldcxt);
	}
	else
	{
		/*
		 * 假设 tupdesc 将超过此扩展对象的生存期，就像
		 * 我们假设它将超过源对象的生存期一样。
		 */
		fc_erh->er_tupdesc = fc_tupdesc;
	}

	/*
	 * 我们不设置 ER_FLAG_DVALUES_VALID 或 ER_FLAG_FVALUE_VALID，因此
	 * 记录在逻辑上仍然是空的。
	 */

	return fc_erh;
}


/*
 * 将给定元组插入为扩展记录的值
 *
 * 确保元组与记录之前分配的行类型相匹配是调用者的责任。 （不过，如果有领域约束，将在这里进行检查。）
 *
 * 如果“copy”为真，则元组将被物理复制到扩展记录的本地存储中，否则调用者必须确保元组的生命期与扩展记录相同。
 *
 * 如果“expand_external”为真，则元组中不在行内的字段值会自动内联，否则不会。 （组合 copy = false，expand_external = true 是不合理且不支持的。）
 *
 * 另外，元组可以为 NULL，在这种情况下，我们只需将扩展记录设置为空。
 */
void expanded_record_set_tuple(ExpandedRecordHeader *fc_erh,
						  HeapTuple fc_tuple,
						  bool fc_copy,
						  bool fc_expand_external)
{
	int			fc_oldflags;
	HeapTuple	fc_oldtuple;
	char	   *fc_oldfstartptr;
	char	   *fc_oldfendptr;
	int			fc_newflags;
	HeapTuple	fc_newtuple;
	MemoryContext fc_oldcxt;

	/* 不应该试图将新数据分配给虚拟头部 */
	Assert(!(fc_erh->flags & ER_FLAG_IS_DUMMY));

	/*
	 * 在执行赋值之前，检查结果是否满足领域要求。
	 */
	if (fc_erh->flags & ER_FLAG_IS_DOMAIN)
		fc_check_domain_for_new_tuple(fc_erh, fc_tuple);

	/*
	 * 如果我们需要去掉外部字段值，就这样做，使用短期上下文来避免泄露
	 * toast fetch 可能产生的杂物。
	 */
	if (fc_expand_external && fc_tuple)
	{
		/* 断言调用者没有请求不支持的情况 */
		Assert(fc_copy);
		if (HeapTupleHasExternal(fc_tuple))
		{
			fc_oldcxt = MemoryContextSwitchTo(get_short_term_cxt(fc_erh));
			fc_tuple = toast_flatten_tuple(fc_tuple, fc_erh->er_tupdesc);
			MemoryContextSwitchTo(fc_oldcxt);
		}
		else
			fc_expand_external = false;	/* 下面不需要进行清理 */
	}

	/*
	 * 初始化新标志，仅保留非数据状态位。
	 */
	fc_oldflags = fc_erh->flags;
	fc_newflags = fc_oldflags & ER_FLAGS_NON_DATA;

	/*
	 * 如果需要，将元组复制到本地存储中。我们必须确保在开始修改扩展记录的状态之前
	 * 这一操作成功。
	 */
	if (fc_copy && fc_tuple)
	{
		fc_oldcxt = MemoryContextSwitchTo(fc_erh->hdr.eoh_context);
		fc_newtuple = heap_copytuple(fc_tuple);
		fc_newflags |= ER_FLAG_FVALUE_ALLOCED;
		MemoryContextSwitchTo(fc_oldcxt);

		/* 现在我们可以刷新任何 detoasting 可能泄露的内容。 */
		if (fc_expand_external)
			MemoryContextReset(fc_erh->er_short_term_cxt);
	}
	else
		fc_newtuple = fc_tuple;

	/* 复制我们即将覆盖的字段 */
	fc_oldtuple = fc_erh->fvalue;
	fc_oldfstartptr = fc_erh->fstartptr;
	fc_oldfendptr = fc_erh->fendptr;

	/*
	 * 现在安全更新扩展记录的状态了。
	 */
	if (fc_newtuple)
	{
		/* 保存扁平表示 */
		fc_erh->fvalue = fc_newtuple;
		fc_erh->fstartptr = (char *) fc_newtuple->t_data;
		fc_erh->fendptr = ((char *) fc_newtuple->t_data) + fc_newtuple->t_len;
		fc_newflags |= ER_FLAG_FVALUE_VALID;

		/* 记住是否有任何外部字段值 */
		if (HeapTupleHasExternal(fc_newtuple))
			fc_newflags |= ER_FLAG_HAVE_EXTERNAL;
	}
	else
	{
		fc_erh->fvalue = NULL;
		fc_erh->fstartptr = fc_erh->fendptr = NULL;
	}

	fc_erh->flags = fc_newflags;

	/* 重置扁平大小信息；我们现在不打算使其有效 */
	fc_erh->flat_size = 0;

	/*
	 * 现在，释放属于旧字段值的任何存储。这样做是安全的，因为 ER_FLAG_DVALUES_VALID
	 * 不再设置在 erh->flags 中；即使我们在途中失败，记录也是有效的，最多我们只是在清理
	 * 一些空间时没有成功。
	 */
	if (fc_oldflags & ER_FLAG_DVALUES_ALLOCED)
	{
		TupleDesc	fc_tupdesc = fc_erh->er_tupdesc;
		int			fc_i;

		for (fc_i = 0; fc_i < fc_erh->nfields; fc_i++)
		{
			if (!fc_erh->dnulls[fc_i] &&
				!(TupleDescAttr(fc_tupdesc, fc_i)->attbyval))
			{
				char	   *fc_oldValue = (char *) DatumGetPointer(fc_erh->dvalues[fc_i]);

				if (fc_oldValue < fc_oldfstartptr || fc_oldValue >= fc_oldfendptr)
					pfree(fc_oldValue);
			}
		}
	}

	/* 同样，释放旧元组（如果是本地分配的） */
	if (fc_oldflags & ER_FLAG_FVALUE_ALLOCED)
		heap_freetuple(fc_oldtuple);

	/* 我们不会在需要之前制作新的去构造表示 */
}

/*
 * make_expanded_record_from_datum: 从复合 Datum 构建扩展记录
 *
 * 这结合了 make_expanded_record_from_typeid 和
 * expanded_record_set_tuple 的功能。然而，我们并不强制立即查找 tupdesc，
 * 理由是它可能根本不需要。
 *
 * 扩展对象将是 parentcontext 的子级。
 *
 * 注意：复合 datum 不能自我识别为领域类型，
 * 所以我们不需要在这里考虑领域情况。
 */
Datum make_expanded_record_from_datum(Datum fc_recorddatum, MemoryContext fc_parentcontext)
{
	ExpandedRecordHeader *fc_erh;
	HeapTupleHeader fc_tuphdr;
	HeapTupleData fc_tmptup;
	HeapTuple	fc_newtuple;
	MemoryContext fc_objcxt;
	MemoryContext fc_oldcxt;

	/*
	 * 为扩展对象分配私有上下文。我们使用常规大小的
	 * 上下文，而不是小型上下文，以提高我们能够在其中容纳 tupdesc
	 * 而无需额外的 malloc 块的几率。
	 */
	fc_objcxt = AllocSetContextCreate(fc_parentcontext,
								   "expanded record",
								   ALLOCSET_DEFAULT_SIZES);

	/* 设置扩展记录头，初始化字段为 0/null */
	fc_erh = (ExpandedRecordHeader *)
		MemoryContextAllocZero(fc_objcxt, sizeof(ExpandedRecordHeader));

	EOH_init_header(&fc_erh->hdr, &ER_methods, fc_objcxt);
	fc_erh->er_magic = ER_MAGIC;

	/*
	 * 在私有上下文中 detoast 并复制源记录，作为 HeapTuple。
	 * （如果我们确实需要 detoast 源，我们将在调用者的上下文中泄露一些内存，
	 * 但似乎没有必要担心这些。）
	 */
	fc_tuphdr = DatumGetHeapTupleHeader(fc_recorddatum);

	fc_tmptup.t_len = HeapTupleHeaderGetDatumLength(fc_tuphdr);
	ItemPointerSetInvalid(&(fc_tmptup.t_self));
	fc_tmptup.t_tableOid = InvalidOid;
	fc_tmptup.t_data = fc_tuphdr;

	fc_oldcxt = MemoryContextSwitchTo(fc_objcxt);
	fc_newtuple = heap_copytuple(&fc_tmptup);
	fc_erh->flags |= ER_FLAG_FVALUE_ALLOCED;
	MemoryContextSwitchTo(fc_oldcxt);

	/* 填充复合类型识别信息 */
	fc_erh->er_decltypeid = fc_erh->er_typeid = HeapTupleHeaderGetTypeId(fc_tuphdr);
	fc_erh->er_typmod = HeapTupleHeaderGetTypMod(fc_tuphdr);

	/* 记住我们有一个平面表示 */
	fc_erh->fvalue = fc_newtuple;
	fc_erh->fstartptr = (char *) fc_newtuple->t_data;
	fc_erh->fendptr = ((char *) fc_newtuple->t_data) + fc_newtuple->t_len;
	fc_erh->flags |= ER_FLAG_FVALUE_VALID;

	/* 不需要设置 ER_FLAG_HAVE_EXTERNAL */
	Assert(!HeapTupleHeaderHasExternal(fc_tuphdr));

	/*
	 * 我们不会在必须时查找 tupdesc 也不会制作去构造表示。我们也没有足够的信息
	 * 来填充 flat_size 和相关字段。
	 */

	/* 返回扩展记录的 R/W 指针 */
	return EOHPGetRWDatum(&fc_erh->hdr);
}

/*
 * 扩展记录的 get_flat_size 方法
 *
 * 注意：在一个合理短暂的内存上下文中调用此方法，以防止由于
 * detoasting 等活动导致内存泄漏。
 */
static Size fc_ER_get_flat_size(ExpandedObjectHeader *fc_eohptr)
{
	ExpandedRecordHeader *fc_erh = (ExpandedRecordHeader *) fc_eohptr;
	TupleDesc	fc_tupdesc;
	Size		fc_len;
	Size		fc_data_len;
	int			fc_hoff;
	bool		fc_hasnull;
	int			fc_i;

	Assert(fc_erh->er_magic == ER_MAGIC);

	/*
	 * 扁平表示必须是有效的复合 datum。确保我们有注册的、而不是匿名的 RECORD 类型。
	 */
	if (fc_erh->er_typeid == RECORDOID &&
		fc_erh->er_typmod < 0)
	{
		fc_tupdesc = expanded_record_get_tupdesc(fc_erh);
		assign_record_type_typmod(fc_tupdesc);
		fc_erh->er_typmod = fc_tupdesc->tdtypmod;
	}

	/*
	 * 如果我们有一个有效的扁平值且没有外部字段，可以直接使用它。
	 */
	if (fc_erh->flags & ER_FLAG_FVALUE_VALID &&
		!(fc_erh->flags & ER_FLAG_HAVE_EXTERNAL))
		return fc_erh->fvalue->t_len;

	/* 如果我们有缓存的大小值，请相信它 */
	if (fc_erh->flat_size)
		return fc_erh->flat_size;

	/* 如果我们还没有去构造元组，就执行该操作 */
	if (!(fc_erh->flags & ER_FLAG_DVALUES_VALID))
		deconstruct_expanded_record(fc_erh);

	/* 元组描述符必须是有效的 */
	fc_tupdesc = fc_erh->er_tupdesc;

	/*
	 * 复合 datums 不能包含任何外部值。
	 */
	if (fc_erh->flags & ER_FLAG_HAVE_EXTERNAL)
	{
		for (fc_i = 0; fc_i < fc_erh->nfields; fc_i++)
		{
			Form_pg_attribute fc_attr = TupleDescAttr(fc_tupdesc, fc_i);

			if (!fc_erh->dnulls[fc_i] &&
				!fc_attr->attbyval && fc_attr->attlen == -1 &&
				VARATT_IS_EXTERNAL(DatumGetPointer(fc_erh->dvalues[fc_i])))
			{
				/*
				 * expanded_record_set_field_internal 可以执行实际的
				 * detoasting 工作。它不需要重新检查领域约束。
				 */
				expanded_record_set_field_internal(fc_erh, fc_i + 1,
												   fc_erh->dvalues[fc_i], false,
												   true,
												   false);
			}
		}

		/*
		 * 我们现在已经删除了所有外部字段值，因此可以清除关于它们的标志。
		 * 这不会导致 ER_flatten_into() 错误地快速处理，因为 expanded_record_set_field()
		 * 将清除 ER_FLAG_FVALUE_VALID。
		 */
		fc_erh->flags &= ~ER_FLAG_HAVE_EXTERNAL;
	}

	/* 测试我们目前是否有任何空值 */
	fc_hasnull = false;
	for (fc_i = 0; fc_i < fc_erh->nfields; fc_i++)
	{
		if (fc_erh->dnulls[fc_i])
		{
			fc_hasnull = true;
			break;
		}
	}

	/* 确定所需的总空间 */
	fc_len = offsetof(HeapTupleHeaderData, t_bits);

	if (fc_hasnull)
		fc_len += BITMAPLEN(fc_tupdesc->natts);

	fc_hoff = fc_len = MAXALIGN(fc_len); /* 安全对齐用户数据 */

	fc_data_len = heap_compute_data_size(fc_tupdesc, fc_erh->dvalues, fc_erh->dnulls);

	fc_len += fc_data_len;

	/* 下次缓存 */
	fc_erh->flat_size = fc_len;
	fc_erh->data_len = fc_data_len;
	fc_erh->hoff = fc_hoff;
	fc_erh->hasnull = fc_hasnull;

	return fc_len;
}

/* 
 * flatten_into 方法用于扩展记录
 */
static void fc_ER_flatten_into(ExpandedObjectHeader *fc_eohptr,
				void *fc_result, Size fc_allocated_size)
{
	ExpandedRecordHeader *fc_erh = (ExpandedRecordHeader *) fc_eohptr;
	HeapTupleHeader fc_tuphdr = (HeapTupleHeader) fc_result;
	TupleDesc	fc_tupdesc;

	Assert(fc_erh->er_magic == ER_MAGIC);

	/* 如果我们有一个有效的扁平化值且没有外部字段，这很简单 */
	if (fc_erh->flags & ER_FLAG_FVALUE_VALID &&
		!(fc_erh->flags & ER_FLAG_HAVE_EXTERNAL))
	{
		Assert(fc_allocated_size == fc_erh->fvalue->t_len);
		memcpy(fc_tuphdr, fc_erh->fvalue->t_data, fc_allocated_size);
		/* 原始的扁平化值可能没有 datum 头字段 */
		HeapTupleHeaderSetDatumLength(fc_tuphdr, fc_allocated_size);
		HeapTupleHeaderSetTypeId(fc_tuphdr, fc_erh->er_typeid);
		HeapTupleHeaderSetTypMod(fc_tuphdr, fc_erh->er_typmod);
		return;
	}

	/* 否则分配应与先前的 get_flat_size 结果匹配 */
	Assert(fc_allocated_size == fc_erh->flat_size);

	/* 我们需要元组描述符 */
	fc_tupdesc = expanded_record_get_tupdesc(fc_erh);

	/* 我们必须确保任何填充空间为零填充 */
	memset(fc_tuphdr, 0, fc_allocated_size);

	/* 设置复合 Datum 的头字段 */
	HeapTupleHeaderSetDatumLength(fc_tuphdr, fc_allocated_size);
	HeapTupleHeaderSetTypeId(fc_tuphdr, fc_erh->er_typeid);
	HeapTupleHeaderSetTypMod(fc_tuphdr, fc_erh->er_typmod);
	/* 我们还确保t_ctid无效，除非明确设置 */
	ItemPointerSetInvalid(&(fc_tuphdr->t_ctid));

	HeapTupleHeaderSetNatts(fc_tuphdr, fc_tupdesc->natts);
	fc_tuphdr->t_hoff = fc_erh->hoff;

	/* 并从 dvalues/dnulls 填充数据区域 */
	heap_fill_tuple(fc_tupdesc,
					fc_erh->dvalues,
					fc_erh->dnulls,
					(char *) fc_tuphdr + fc_erh->hoff,
					fc_erh->data_len,
					&fc_tuphdr->t_infomask,
					(fc_erh->hasnull ? fc_tuphdr->t_bits : NULL));
}

/* 
 * 查看扩展记录的实际类型的 tupdesc
 * 
 * 注意：在此模块内部的代码可以仅在设置 ER_FLAG_DVALUES_VALID 时获取
 * erh->er_tupdesc；否则应调用 expanded_record_get_tupdesc。此函数是
 * expanded_record_get_tupdesc 的外部部分。
 */
TupleDesc expanded_record_fetch_tupdesc(ExpandedRecordHeader *fc_erh)
{
	TupleDesc	fc_tupdesc;

	/* 如果我们已经拥有它，这很简单（但调用者应该已经检查过） */
	if (fc_erh->er_tupdesc)
		return fc_erh->er_tupdesc;

	/* 使用 typcache 查找复合类型的 tupdesc */
	fc_tupdesc = lookup_rowtype_tupdesc(fc_erh->er_typeid, fc_erh->er_typmod);

	/* 
 * 如果它是一个引用计数的 tupdesc 而不是一个静态分配的，我们
 * 希望通过内存上下文回调来管理引用计数，而不是假设 CurrentResourceOwner
 * 的生命周期比这个扩展对象更长。
 */
	if (fc_tupdesc->tdrefcount >= 0)
	{
		/* 如果我们还没有注册回调，则注册回调 */
		if (fc_erh->er_mcb.arg == NULL)
		{
			fc_erh->er_mcb.func = fc_ER_mc_callback;
			fc_erh->er_mcb.arg = (void *) fc_erh;
			MemoryContextRegisterResetCallback(fc_erh->hdr.eoh_context,
											   &fc_erh->er_mcb);
		}

		/* 记住我们自己的指针 */
		fc_erh->er_tupdesc = fc_tupdesc;
		fc_tupdesc->tdrefcount++;

		/* 释放获取的 pin lookup_rowtype_tupdesc */
		ReleaseTupleDesc(fc_tupdesc);
	}
	else
	{
		/* 只需记住指针 */
		fc_erh->er_tupdesc = fc_tupdesc;
	}

	/* 无论如何，获取该 tupdesc 的进程全局 ID */
	fc_erh->er_tupdesc_id = assign_record_type_identifier(fc_tupdesc->tdtypeid,
													   fc_tupdesc->tdtypmod);

	return fc_tupdesc;
}

/* 
 * 获取表示扩展记录当前值的 HeapTuple
 * 
 * 如果有效，则返回原始存储的元组，因此调用者不得
 * 修改它。否则，我们返回在当前内存上下文中创建的 HeapTuple。
 * 在这两种情况下，都没有尝试将外部烤面的值内联化，因此该元组不能作为复合 datum 使用。
 * 
 * 如果扩展记录为空，则返回 NULL。
 */
HeapTuple expanded_record_get_tuple(ExpandedRecordHeader *fc_erh)
{
	/* 如果我们仍然有原始元组，这很简单 */
	if (fc_erh->flags & ER_FLAG_FVALUE_VALID)
		return fc_erh->fvalue;

	/* 否则，只需从 datum 中构建一个元组 */
	if (fc_erh->flags & ER_FLAG_DVALUES_VALID)
		return heap_form_tuple(fc_erh->er_tupdesc, fc_erh->dvalues, fc_erh->dnulls);

	/* 扩展记录为空 */
	return NULL;
}

/* 
 * 用于清理外部资源的内存上下文重置回调 */
static void fc_ER_mc_callback(void *fc_arg)
{
	ExpandedRecordHeader *fc_erh = (ExpandedRecordHeader *) fc_arg;
	TupleDesc	fc_tupdesc = fc_erh->er_tupdesc;

	/* 如果有，则释放我们私有管理的 tupdesc 引用计数 */
	if (fc_tupdesc)
	{
		fc_erh->er_tupdesc = NULL; /* 只是运气 */
		if (fc_tupdesc->tdrefcount > 0)
		{
			if (--fc_tupdesc->tdrefcount == 0)
				FreeTupleDesc(fc_tupdesc);
		}
	}
}

/* 
 * DatumGetExpandedRecord：从输入参数获取可写的扩展记录
 * 
 * 注意：如果输入是一个读/写指针，则返回输入
 * 参数；因此调用者必须确保他们的更改是“安全的”，也就是说
 * 他们不能将记录留在损坏的状态。
 */
ExpandedRecordHeader * DatumGetExpandedRecord(Datum fc_d)
{
	/* 如果已经是可写的扩展记录，则直接返回它 */
	if (VARATT_IS_EXTERNAL_EXPANDED_RW(DatumGetPointer(fc_d)))
	{
		ExpandedRecordHeader *fc_erh = (ExpandedRecordHeader *) DatumGetEOHP(fc_d);

		Assert(fc_erh->er_magic == ER_MAGIC);
		return fc_erh;
	}

	/* 否则以更困难的方式扩展 */
	fc_d = make_expanded_record_from_datum(fc_d, CurrentMemoryContext);
	return (ExpandedRecordHeader *) DatumGetEOHP(fc_d);
}

/* 
 * 创建 Datum/isnull 表示的扩展记录对象
 * 如果我们还没有这样做。在调用此函数后，直接读取
 * dvalues/dnulls 数组是可以的，而不是通过 get_field。
 * 
 * 请注意，如果该对象当前为空（“null”），这将将其更改
 * 为表示一行 null。
 */
void deconstruct_expanded_record(ExpandedRecordHeader *fc_erh)
{
	TupleDesc	fc_tupdesc;
	Datum	   *fc_dvalues;
	bool	   *fc_dnulls;
	int			fc_nfields;

	if (fc_erh->flags & ER_FLAG_DVALUES_VALID)
		return;					/* 已经有效，无需操作 */

	/* 我们需要元组描述符 */
	fc_tupdesc = expanded_record_get_tupdesc(fc_erh);

	/* 
 * 如果我们还没有，则在私有上下文中分配数组。我们
 * 不期望在这里看到 nfields 的变化，因此虽然我们可以应对
 * 如果发生变化，但我们不妨去避免旧数组的泄漏（这些可能
 * 本来就没有单独分配）。
 */
	fc_nfields = fc_tupdesc->natts;
	if (fc_erh->dvalues == NULL || fc_erh->nfields != fc_nfields)
	{
		char	   *fc_chunk;

		/* 
 * 为了节省 palloc 周期，我们将 Datum 和 isnull
 * 数组在一个 palloc 块中分配。
 */ 
		fc_chunk = MemoryContextAlloc(fc_erh->hdr.eoh_context,
								   fc_nfields * (sizeof(Datum) + sizeof(bool)));
		fc_dvalues = (Datum *) fc_chunk;
		fc_dnulls = (bool *) (fc_chunk + fc_nfields * sizeof(Datum));
		fc_erh->dvalues = fc_dvalues;
		fc_erh->dnulls = fc_dnulls;
		fc_erh->nfields = fc_nfields;
	}
	else
	{
		fc_dvalues = fc_erh->dvalues;
		fc_dnulls = fc_erh->dnulls;
	}

	if (fc_erh->flags & ER_FLAG_FVALUE_VALID)
	{
		/* 解构元组 */
		heap_deform_tuple(fc_erh->fvalue, fc_tupdesc, fc_dvalues, fc_dnulls);
	}
	else
	{
		/* 如果记录为空，将其实例化为一行空值 */
		memset(fc_dvalues, 0, fc_nfields * sizeof(Datum));
		memset(fc_dnulls, true, fc_nfields * sizeof(bool));
	}

	/* 将dvalues标记为有效 */
	fc_erh->flags |= ER_FLAG_DVALUES_VALID;
}

/*
 * 按名称查找记录字段
 *
 * 如果存在名为“fieldname”的字段，填充finfo的内容
 * 并返回“true”。否则返回“false”，并且不更改*finfo。
 */
bool expanded_record_lookup_field(ExpandedRecordHeader *fc_erh, const char *fc_fieldname,
							 ExpandedRecordFieldInfo *fc_finfo)
{
	TupleDesc	fc_tupdesc;
	int			fc_fno;
	Form_pg_attribute fc_attr;
	const FormData_pg_attribute *fc_sysattr;

	fc_tupdesc = expanded_record_get_tupdesc(fc_erh);

	/* 首先检查用户定义的属性 */
	for (fc_fno = 0; fc_fno < fc_tupdesc->natts; fc_fno++)
	{
		fc_attr = TupleDescAttr(fc_tupdesc, fc_fno);
		if (namestrcmp(&fc_attr->attname, fc_fieldname) == 0 &&
			!fc_attr->attisdropped)
		{
			fc_finfo->fnumber = fc_attr->attnum;
			fc_finfo->ftypeid = fc_attr->atttypid;
			fc_finfo->ftypmod = fc_attr->atttypmod;
			fc_finfo->fcollation = fc_attr->attcollation;
			return true;
		}
	}

	/* 系统属性呢？ */
	fc_sysattr = SystemAttributeByName(fc_fieldname);
	if (fc_sysattr != NULL)
	{
		fc_finfo->fnumber = fc_sysattr->attnum;
		fc_finfo->ftypeid = fc_sysattr->atttypid;
		fc_finfo->ftypmod = fc_sysattr->atttypmod;
		fc_finfo->fcollation = fc_sysattr->attcollation;
		return true;
	}

	return false;
}

/*
 * 获取记录字段的值
 *
 * expanded_record_get_field是该前端，它处理
 * 简单的可内联情况。
 */
Datum expanded_record_fetch_field(ExpandedRecordHeader *fc_erh, int fc_fnumber,
							bool *fc_isnull)
{
	if (fc_fnumber > 0)
	{
		/* 空记录具有空字段 */
		if (ExpandedRecordIsEmpty(fc_erh))
		{
			*fc_isnull = true;
			return (Datum) 0;
		}
		/* 确保我们已经解构了形式 */
		deconstruct_expanded_record(fc_erh);
		/* 超出范围的字段号码读取为空 */
		if (unlikely(fc_fnumber > fc_erh->nfields))
		{
			*fc_isnull = true;
			return (Datum) 0;
		}
		*fc_isnull = fc_erh->dnulls[fc_fnumber - 1];
		return fc_erh->dvalues[fc_fnumber - 1];
	}
	else
	{
		/* 如果我们没有平坦元组，系统列读取为空 */
		if (fc_erh->fvalue == NULL)
		{
			*fc_isnull = true;
			return (Datum) 0;
		}
		/* heap_getsysattr其实不使用tupdesc，所以只需传递null */
		return heap_getsysattr(fc_erh->fvalue, fc_fnumber, NULL, fc_isnull);
	}
}

/*
 * 设置记录字段的值
 *
 * 如果扩展记录是域类型，赋值将被拒绝
 * （不更改记录的状态），如果违反了域的约束。
 *
 * 如果expand_external为true且newValue是离线值，我们将
 * 强制解压，以便记录不依赖于外部存储。
 *
 * 内部调用者可以传递check_constraints = false以跳过应用
 * 域约束。外部调用者不应这样做。
 */
void expanded_record_set_field_internal(ExpandedRecordHeader *fc_erh, int fc_fnumber,
								   Datum fc_newValue, bool fc_isnull,
								   bool fc_expand_external,
								   bool fc_check_constraints)
{
	TupleDesc	fc_tupdesc;
	Form_pg_attribute fc_attr;
	Datum	   *fc_dvalues;
	bool	   *fc_dnulls;
	char	   *fc_oldValue;

	/*
	 * 除非在进行字段内联的内部调用情况下，
	 * 否则不应该尝试向虚拟头分配新数据。
	 */
	Assert(!(fc_erh->flags & ER_FLAG_IS_DUMMY) || !fc_check_constraints);

	/* 在执行赋值之前，查看结果是否满足域的要求 */
	if ((fc_erh->flags & ER_FLAG_IS_DOMAIN) && fc_check_constraints)
		fc_check_domain_for_new_field(fc_erh, fc_fnumber, fc_newValue, fc_isnull);

	/* 如果我们还没有去构造元组，就执行该操作 */
	if (!(fc_erh->flags & ER_FLAG_DVALUES_VALID))
		deconstruct_expanded_record(fc_erh);

	/* 元组描述符必须是有效的 */
	fc_tupdesc = fc_erh->er_tupdesc;
	Assert(fc_erh->nfields == fc_tupdesc->natts);

	/* 如果fnumber是系统列或不存在的列，则调用者会出错 */
	if (unlikely(fc_fnumber <= 0 || fc_fnumber > fc_erh->nfields))
		elog(ERROR, "cannot assign to field %d of expanded record", fc_fnumber);

	/*
	 * 将新字段值复制到记录的上下文中，并处理解压，
	 * 如果需要的话。
	 */
	fc_attr = TupleDescAttr(fc_tupdesc, fc_fnumber - 1);
	if (!fc_isnull && !fc_attr->attbyval)
	{
		MemoryContext fc_oldcxt;

		/* 如果请求，解压任何外部值 */
		if (fc_expand_external)
		{
			if (fc_attr->attlen == -1 &&
				VARATT_IS_EXTERNAL(DatumGetPointer(fc_newValue)))
			{
				/* 解压应该在短期上下文中进行。 */
				fc_oldcxt = MemoryContextSwitchTo(get_short_term_cxt(fc_erh));
				fc_newValue = PointerGetDatum(detoast_external_attr((struct varlena *) DatumGetPointer(fc_newValue)));
				MemoryContextSwitchTo(fc_oldcxt);
			}
			else
				fc_expand_external = false;	/* 下面不需要进行清理 */
		}

		/* 将值复制到记录的上下文中 */
		fc_oldcxt = MemoryContextSwitchTo(fc_erh->hdr.eoh_context);
		fc_newValue = datumCopy(fc_newValue, false, fc_attr->attlen);
		MemoryContextSwitchTo(fc_oldcxt);

		/* 现在我们可以刷新任何解压可能泄漏的内容 */
		if (fc_expand_external)
			MemoryContextReset(fc_erh->er_short_term_cxt);

		/* 记住我们有可能需要pfree的字段 */
		fc_erh->flags |= ER_FLAG_DVALUES_ALLOCED;

		/*
		 * 当我们在这里时，注意这是否是外部加热值，
		 * 因为那可能意味着我们需要稍后内联它。 （请勿考虑
		 * 将其合并到之前的expand_external逻辑中：datumCopy本身可能
		 * 已经使值变为非外部。）
		 */
		if (fc_attr->attlen == -1 &&
			VARATT_IS_EXTERNAL(DatumGetPointer(fc_newValue)))
			fc_erh->flags |= ER_FLAG_HAVE_EXTERNAL;
	}

	/*
	 * 我们准备进行不可逆的更改。
	 */
	fc_dvalues = fc_erh->dvalues;
	fc_dnulls = fc_erh->dnulls;

	/* 扁平化的值将不再准确地表示记录 */
	fc_erh->flags &= ~ER_FLAG_FVALUE_VALID;
	/* 而且我们也不知道扁平化的大小 */
	fc_erh->flat_size = 0;

	/* 如果需要，则获取旧字段值以进行pfree。 */
	if (!fc_attr->attbyval && !fc_dnulls[fc_fnumber - 1])
		fc_oldValue = (char *) DatumGetPointer(fc_dvalues[fc_fnumber - 1]);
	else
		fc_oldValue = NULL;

	/* 最后我们可以插入新字段。 */
	fc_dvalues[fc_fnumber - 1] = fc_newValue;
	fc_dnulls[fc_fnumber - 1] = fc_isnull;

	/*
	 * 如果需要，释放旧字段；这可以防止重复的字段替换
	 * 膨胀记录的存储空间。如果pfree以某种方式失败，它不会
	 * 损坏记录。
	 *
	 * 如果我们正在更新虚拟头，我们不能冒险释放旧
	 * 值，因为最有可能的是扩展记录的主头仍然有
	 * 指向它的指针。这不会导致任何持续的内存泄漏，因为
	 * 我们在这里刚刚分配的内容在短期域检查中。
	 */
	if (fc_oldValue && !(fc_erh->flags & ER_FLAG_IS_DUMMY))
	{
		/* 不要尝试释放原始平面记录的一部分 */
		if (fc_oldValue < fc_erh->fstartptr || fc_oldValue >= fc_erh->fendptr)
			pfree(fc_oldValue);
	}
}

/*
 * 设置所有记录字段
 *
 * 调用者必须确保提供的datums类型正确
 * 与记录先前分配的行类型匹配。
 *
 * 如果expand_external为true，我们将强制解压离线字段值
 * 以便记录不依赖于外部存储。
 *
 * 与重复应用expanded_record_set_field()不同，这不
 * 能保证在出错的情况下将扩展记录保持在非损坏状态。
 * 通常它仅用于初始化新的扩展记录。此外，因为我们
 * 预计在扩展记录的生命周期内最多应用一次，
 * 所以我们不担心解压可能泄漏的任何垃圾。
 */
void expanded_record_set_fields(ExpandedRecordHeader *fc_erh,
						   const Datum *fc_newValues, const bool *fc_isnulls,
						   bool fc_expand_external)
{
	TupleDesc	fc_tupdesc;
	Datum	   *fc_dvalues;
	bool	   *fc_dnulls;
	int			fc_fnumber;
	MemoryContext fc_oldcxt;

	/* 不应该试图将新数据分配给虚拟头部 */
	Assert(!(fc_erh->flags & ER_FLAG_IS_DUMMY));

	/* 如果我们还没有去构造元组，就执行该操作 */
	if (!(fc_erh->flags & ER_FLAG_DVALUES_VALID))
		deconstruct_expanded_record(fc_erh);

	/* 元组描述符必须是有效的 */
	fc_tupdesc = fc_erh->er_tupdesc;
	Assert(fc_erh->nfields == fc_tupdesc->natts);

	/* 扁平化的值将不再准确地表示记录 */
	fc_erh->flags &= ~ER_FLAG_FVALUE_VALID;
	/* 而且我们也不知道扁平化的大小 */
	fc_erh->flat_size = 0;

	fc_oldcxt = MemoryContextSwitchTo(fc_erh->hdr.eoh_context);

	fc_dvalues = fc_erh->dvalues;
	fc_dnulls = fc_erh->dnulls;

	for (fc_fnumber = 0; fc_fnumber < fc_erh->nfields; fc_fnumber++)
	{
		Form_pg_attribute fc_attr = TupleDescAttr(fc_tupdesc, fc_fnumber);
		Datum		fc_newValue;
		bool		fc_isnull;

		/* 忽略已删除的列 */
		if (fc_attr->attisdropped)
			continue;

		fc_newValue = fc_newValues[fc_fnumber];
		fc_isnull = fc_isnulls[fc_fnumber];

		if (!fc_attr->attbyval)
		{
			/*
			 * 将新字段值复制到记录的上下文中，如有必要，进行
			 * 解压缩处理。
			 */
			if (!fc_isnull)
			{
				/* 这是一个外部的压缩值吗？ */
				if (fc_attr->attlen == -1 &&
					VARATT_IS_EXTERNAL(DatumGetPointer(fc_newValue)))
				{
					if (fc_expand_external)
					{
						/* 在复制值时按需解压缩 */
						fc_newValue = PointerGetDatum(detoast_external_attr((struct varlena *) DatumGetPointer(fc_newValue)));
					}
					else
					{
						/* 只是复制值 */
						fc_newValue = datumCopy(fc_newValue, false, -1);
						/* 如果仍然是外部的，请记住这一点 */
						if (VARATT_IS_EXTERNAL(DatumGetPointer(fc_newValue)))
							fc_erh->flags |= ER_FLAG_HAVE_EXTERNAL;
					}
				}
				else
				{
					/* 不是外部值，只需复制 */
					fc_newValue = datumCopy(fc_newValue, false, fc_attr->attlen);
				}

				/* 记住我们有字段需要释放内存 */
				fc_erh->flags |= ER_FLAG_DVALUES_ALLOCED;
			}

			/*
			 * 释放旧字段值（如果有的话，可能性不大，因为我们
			 * 应该是在一个空记录中插入）。
			 */
			if (unlikely(!fc_dnulls[fc_fnumber]))
			{
				char	   *fc_oldValue;

				fc_oldValue = (char *) DatumGetPointer(fc_dvalues[fc_fnumber]);
				/* 不要尝试释放原始平面记录的一部分 */
				if (fc_oldValue < fc_erh->fstartptr || fc_oldValue >= fc_erh->fendptr)
					pfree(fc_oldValue);
			}
		}

		/* 最后我们可以插入新字段。 */
		fc_dvalues[fc_fnumber] = fc_newValue;
		fc_dnulls[fc_fnumber] = fc_isnull;
	}

	/*
	 * 因为我们不保证 set_fields() 的原子性，所以我们可以
	 * 将域约束的检查留到最后一步；如果抛出错误，那就糟糕了。
	 */
	if (fc_erh->flags & ER_FLAG_IS_DOMAIN)
	{
		/* 我们在短期上下文中运行域检查以限制垃圾 */
		MemoryContextSwitchTo(get_short_term_cxt(fc_erh));

		domain_check(ExpandedRecordGetRODatum(fc_erh), false,
					 fc_erh->er_decltypeid,
					 &fc_erh->er_domaininfo,
					 fc_erh->hdr.eoh_context);
	}

	MemoryContextSwitchTo(fc_oldcxt);
}

/*
 * 构造（或重置）用于短期操作的工作内存上下文。
 *
 * 此上下文用于域检查评估和解压缩。
 *
 * 如果我们没有短期内存上下文，则创建一个；如果已有，重置
 * 它以清除任何残留的垃圾。（为了这个目的需要一个完整的上下文
 * 有点烦人，因为它通常不会使用——但否则很难避免内存泄漏。
 * 至少可以使上下文变小。）
 */
static MemoryContext get_short_term_cxt(ExpandedRecordHeader *fc_erh)
{
	if (fc_erh->er_short_term_cxt == NULL)
		fc_erh->er_short_term_cxt =
			AllocSetContextCreate(fc_erh->hdr.eoh_context,
								  "expanded record short-term context",
								  ALLOCSET_SMALL_SIZES);
	else
		MemoryContextReset(fc_erh->er_short_term_cxt);
	return fc_erh->er_short_term_cxt;
}

/*
 * 构造“虚拟头”以检查域约束。
 *
 * 由于我们不想修改扩展记录的状态，直到我们验证了约束，
 * 我们的方法是设置一个包含新字段值的虚拟记录头，然后将其传递
 * 给域检查。我们将虚拟头保留作为扩展记录状态的一部分，以节省
 * 内存分配周期，但在每次使用时重新初始化（大部分）内容。
 */
static void fc_build_dummy_expanded_header(ExpandedRecordHeader *fc_main_erh)
{
	ExpandedRecordHeader *fc_erh;
	TupleDesc	fc_tupdesc = expanded_record_get_tupdesc(fc_main_erh);

	/* 确保我们有一个短期上下文 */
	(void) get_short_term_cxt(fc_main_erh);

	/*
	 * 在首次通过时分配虚拟头，或者在字段数改变的情况下（在这种情况
	 * 下我们只需泄漏旧的）。在请求中包括其字段值的空间。
	 */
	fc_erh = fc_main_erh->er_dummy_header;
	if (fc_erh == NULL || fc_erh->nfields != fc_tupdesc->natts)
	{
		char	   *fc_chunk;

		fc_erh = (ExpandedRecordHeader *)
			MemoryContextAlloc(fc_main_erh->hdr.eoh_context,
							   MAXALIGN(sizeof(ExpandedRecordHeader))
							   + fc_tupdesc->natts * (sizeof(Datum) + sizeof(bool)));

		/* 确保所有头字段初始化为 0/null */
		memset(fc_erh, 0, sizeof(ExpandedRecordHeader));

		/*
		 * 我们设置虚拟头，使其内存上下文是短期上下文。这是为了
		 * 确保如果因尝试从虚拟头提取复合数据而发生任何外部值的
		 * 解压缩，解压缩的内容将进入短期上下文而不会引起泄漏。
		 * 这在扩展对象协议上有点不规范；但是，因为我们从未将
		 * 一个读/写指针传递给任何其他代码，其他任何东西都无权删除
		 * 或转移对象上下文的所有权，所以应该是相当安全的。
		 */
		EOH_init_header(&fc_erh->hdr, &ER_methods, fc_main_erh->er_short_term_cxt);
		fc_erh->er_magic = ER_MAGIC;

		/* 设置 dvalues/dnulls，目前没有有效内容 */
		fc_chunk = (char *) fc_erh + MAXALIGN(sizeof(ExpandedRecordHeader));
		fc_erh->dvalues = (Datum *) fc_chunk;
		fc_erh->dnulls = (bool *) (fc_chunk + fc_tupdesc->natts * sizeof(Datum));
		fc_erh->nfields = fc_tupdesc->natts;

		/*
		 * 我们刚设置的字段假设在多次使用虚拟头检查域约束时保持不变。
		 * 所有其他虚拟头字段应在下面明确重置，以确保不发生一次
		 * 检查对下一次检查的意外影响。
		 */

		fc_main_erh->er_dummy_header = fc_erh;
	}

	/*
	 * 如果有人询问虚拟头声明的类型，它应该报告复合基础类型，
	 * 而不是域类型（因为域检查约束中的值是基础类型，而不是域类型）。
	 * 因此，我们不传递 IS_DOMAIN 标志，实际上也不传递主要头部的
	 * 任何标志，因为此时虚拟头没有数据。但不要忘记将头标记为虚拟。
	 */
	fc_erh->flags = ER_FLAG_IS_DUMMY;

	/* 复制复合类型识别信息 */
	fc_erh->er_decltypeid = fc_erh->er_typeid = fc_main_erh->er_typeid;
	fc_erh->er_typmod = fc_main_erh->er_typmod;

	/* 虚拟头不需要自己的 tupdesc 引用计数 */
	fc_erh->er_tupdesc = fc_tupdesc;
	fc_erh->er_tupdesc_id = fc_main_erh->er_tupdesc_id;

	/*
	 * 我们很想复制我们所知道的平面大小，但没有意义，因为
	 * 我们肯定要修改虚拟记录的字段。相反，仅清除上一次使用
	 * 循环中的任何剩余内容。
	 */
	fc_erh->flat_size = 0;

	/* 如果我们有 fvalue，就复制过来，以便系统列可用 */
	fc_erh->fvalue = fc_main_erh->fvalue;
	fc_erh->fstartptr = fc_main_erh->fstartptr;
	fc_erh->fendptr = fc_main_erh->fendptr;
}

/*
 * 在 set_field 操作之前检查域约束
 */
static pg_noinline void
fc_check_domain_for_new_field(ExpandedRecordHeader *fc_erh, int fc_fnumber,
						   Datum fc_newValue, bool fc_isnull)
{
	ExpandedRecordHeader *fc_dummy_erh;
	MemoryContext fc_oldcxt;

	/* 构造虚拟头以包含提议的新字段集 */
	fc_build_dummy_expanded_header(fc_erh);
	fc_dummy_erh = fc_erh->er_dummy_header;

	/*
	 * 如果记录不为空，则仅解构（如有需要）并复制现有字段值。
	 * 如果为空，则手动将字段填充为 null——不要过早调用
	 * deconstruct_expanded_record。
	 */
	if (!ExpandedRecordIsEmpty(fc_erh))
	{
		deconstruct_expanded_record(fc_erh);
		memcpy(fc_dummy_erh->dvalues, fc_erh->dvalues,
			   fc_dummy_erh->nfields * sizeof(Datum));
		memcpy(fc_dummy_erh->dnulls, fc_erh->dnulls,
			   fc_dummy_erh->nfields * sizeof(bool));
		/* 那里可能有一些外部值... */
		fc_dummy_erh->flags |= fc_erh->flags & ER_FLAG_HAVE_EXTERNAL;
	}
	else
	{
		memset(fc_dummy_erh->dvalues, 0, fc_dummy_erh->nfields * sizeof(Datum));
		memset(fc_dummy_erh->dnulls, true, fc_dummy_erh->nfields * sizeof(bool));
	}

	/* 无论如何，我们现在有有效的 dvalues */
	fc_dummy_erh->flags |= ER_FLAG_DVALUES_VALID;

	/* 如果fnumber是系统列或不存在的列，则调用者会出错 */
	if (unlikely(fc_fnumber <= 0 || fc_fnumber > fc_dummy_erh->nfields))
		elog(ERROR, "cannot assign to field %d of expanded record", fc_fnumber);

	/* 将提议的新值插入虚拟字段数组 */
	fc_dummy_erh->dvalues[fc_fnumber - 1] = fc_newValue;
	fc_dummy_erh->dnulls[fc_fnumber - 1] = fc_isnull;

	/*
	 * 提议的新值可能是外部的，在这种情况下，我们最好在
	 * dummy_erh 中设置标志。（这很重要，以防域检查表达式中的
	 * 某些东西尝试从虚拟头提取平面值。）
	 */
	if (!fc_isnull)
	{
		Form_pg_attribute fc_attr = TupleDescAttr(fc_erh->er_tupdesc, fc_fnumber - 1);

		if (!fc_attr->attbyval && fc_attr->attlen == -1 &&
			VARATT_IS_EXTERNAL(DatumGetPointer(fc_newValue)))
			fc_dummy_erh->flags |= ER_FLAG_HAVE_EXTERNAL;
	}

	/*
	 * 我们在短期上下文中调用域检查，以便可以回收表达式评估
	 * 泄漏的任何垃圾。
	 */
	fc_oldcxt = MemoryContextSwitchTo(fc_erh->er_short_term_cxt);

	
	domain_check(ExpandedRecordGetRODatum(fc_dummy_erh), false,
				 fc_erh->er_decltypeid,
				 &fc_erh->er_domaininfo,
				 fc_erh->hdr.eoh_context);

	MemoryContextSwitchTo(fc_oldcxt);

	/* 我们不妨立即清理无用代码。 */
	MemoryContextReset(fc_erh->er_short_term_cxt);
}

/*
 * 为 set_tuple 操作预检域约束
 */
static pg_noinline void
fc_check_domain_for_new_tuple(ExpandedRecordHeader *fc_erh, HeapTuple fc_tuple)
{
	ExpandedRecordHeader *fc_dummy_erh;
	MemoryContext fc_oldcxt;

	/* 如果我们被告知将记录设置为空，请检查 NULL 是否可以 */
	if (fc_tuple == NULL)
	{
		/* 我们在短期上下文中运行域检查以限制垃圾 */
		fc_oldcxt = MemoryContextSwitchTo(get_short_term_cxt(fc_erh));

		domain_check((Datum) 0, true,
					 fc_erh->er_decltypeid,
					 &fc_erh->er_domaininfo,
					 fc_erh->hdr.eoh_context);

		MemoryContextSwitchTo(fc_oldcxt);

		/* 我们不妨立即清理无用代码。 */
		MemoryContextReset(fc_erh->er_short_term_cxt);

		return;
	}

	/* 构建虚拟头以包含替换元组 */
	fc_build_dummy_expanded_header(fc_erh);
	fc_dummy_erh = fc_erh->er_dummy_header;

	/* 插入元组，但暂时不需要拆解其字段 */
	fc_dummy_erh->fvalue = fc_tuple;
	fc_dummy_erh->fstartptr = (char *) fc_tuple->t_data;
	fc_dummy_erh->fendptr = ((char *) fc_tuple->t_data) + fc_tuple->t_len;
	fc_dummy_erh->flags |= ER_FLAG_FVALUE_VALID;

	/* 记住是否有任何外部字段值 */
	if (HeapTupleHasExternal(fc_tuple))
		fc_dummy_erh->flags |= ER_FLAG_HAVE_EXTERNAL;

	/*
	 * 我们在短期上下文中调用域检查，以便可以回收表达式评估
	 * 泄漏的任何垃圾。
	 */
	fc_oldcxt = MemoryContextSwitchTo(fc_erh->er_short_term_cxt);

	
	domain_check(ExpandedRecordGetRODatum(fc_dummy_erh), false,
				 fc_erh->er_decltypeid,
				 &fc_erh->er_domaininfo,
				 fc_erh->hdr.eoh_context);

	MemoryContextSwitchTo(fc_oldcxt);

	/* 我们不妨立即清理无用代码。 */
	MemoryContextReset(fc_erh->er_short_term_cxt);
}
